async await in swift 6
概要
これ。
https://speakerdeck.com/koher/await
以下書き下し。
今年の秋にはリリースされそうとのこと。
コールバック関数がasync/awaitが入るとこうなるよと。
func download(from url: URL, completion: @escaping (Data) -> Void)
download(from: url) { data in
// trailing closureっていう名前だったんだなこれ
}
func download(from url: URL) async -> Data
let data = await donwload(from: url)
// ここで非同期を受け取れる
tryとthrowsの関係ににているねーっていう。
func foo() throws {}
func bar() {
try foo() // これはおこられる、検査例外なのかな
}
Potential Suspension Point(って何 -> スレッドが占有された状態が解放される可能性があるポイント
func donwload(from url: URL) async -> Data
func downloadImage(from url: URL) async -> UIImage {
// Potential Suspension Point
let data = await download(from: url)// スレッドが占有された状態が解放される可能性があるポイント、なるほど
// ここから先はぜんぜん別のスレッドで実行される可能性がある
let image = UIImage(data: data)!
return image
}
UIスレッドへと反映させるには別の手法を組み合わせるんだろうな、GCDかな。今までできてたことだし。
PromiseやFutureを返さないのか?
出てこない。
一番近いのはKotlinのSuspendFunc// これはちょっと気になる
asyncとawaitは別の型を返している。
func foo() {
var a:() async -> Int = {42}
var b:() -> Int = {42}
a = b// これはコンパイル通るが、逆は無理
var c = await a()
print(c)
}
// いいな~~~これ~~~これUnityでもやりたいな~~~。
同期関数は非同期のサブタイプ、みたいな関係がある。
コールバック版
// サーバーから JSON を取得し、
// User インスタンスをデコードする
// 非同期関数 fetchUser を完成させて下さい。
//
// サーバーから JSON を取得するには
// download 関数を用います。
// download の実装は擬似的なものですが
// 変更せずにそのまま利用して下さい。
//
// なお、通信やデコードに起因するエラーは
// 起こらないものとします。
import Foundation
import FoundationNetworking
struct User: Identifiable, Codable {
typealias ID = Int
let id: ID
var name: String
var thumbnailURL: URL
}
func download(from url: URL, completion: @escaping (Data) -> Void) {
let data: Data = try! Data(contentsOf: url)
completion(data)
}
func fetchUser(for id: User.ID, completion: @escaping (User) -> Void) {
let url: URL = URL(string: "https://koherent.org/async-await-challenge/api/user?id=\(id.description)")!
// 🚧 ここを実装する
download(from: url) { data in
let user = try! JSONDecoder().decode(User.self, from:data)
completion(user)
}
}
fetchUser(for: 123) { user in
print(user.name)
}
asyncによる非同期版
// サーバーから JSON を取得し、
// User インスタンスをデコードする
// 非同期関数 fetchUser を完成させて下さい。
//
// サーバーから JSON を取得するには
// download 関数を用います。
// download の実装は擬似的なものですが
// 変更せずにそのまま利用して下さい。
//
// なお、通信やデコードに起因するエラーは
// 起こらないものとします。
import Foundation
import FoundationNetworking
struct User: Identifiable, Codable {
typealias ID = Int
let id: ID
var name: String
var thumbnailURL: URL
}
func download(from url: URL) async -> Data {
let data: Data = try! Data(contentsOf: url)
return data
}
func fetchUser(for id: User.ID) async -> User {
let url: URL = URL(string: "https://koherent.org/async-await-challenge/api/user?id=\(id.description)")!
// 🚧 ここを実装する
let data = await download(from: url)
let user = try! JSONDecoder().decode(User.self, from:data)
return user
}
// func foo() {
// var a:() async -> Int = {42}
// // var b:() -> Int = {42}
// // a = b// これはなんかsyncがasyncのサブタイプであることをコンパイルが通るから証明できてるねみたいな例。逆 b = aはコンパイルエラー。
// var c = await a()
// print(c)
// }
runAsyncAndBlock {
// foo()
let user = await fetchUser(for: 123)
print(user.name)
}
エラーが起こり得る場合
func download(from url: URL) async throws -> Data
do {
let data = try await download(from: url)
// ここでdataを使う
} catch {
// ここでエラー処理を行う
}
先にawaitが行われてtryが行われるので、キーワードはasync throws という順番になる。これは固定。
めちゃくちゃ合理的。
catchを合成できる -> 複数のエラーを同じcatchで扱える / 扱わないといけなくなるので、うーん一長一短
ネストが解消できる + エラーハンドリングが収束される
pros-consあるけど比較しづらいなー。
並行処理をどうすればいいか
複数のgo 関数みたいなやつ
関数aとbを並行に実行したい = 同時に待ちたい、みたいなケースをどうするか
async let a = foo()// awaitをつける必要がない
async let b = bar()// awaitをつける必要がない
print(await a + b)// まとめてawaitする
JSの場合は、asyncな関数は必ずpromiseを返してくるので、個々にawaitで剥がす形になる。
print(await a + await b)とかになる。
JSの場合はpromise型を返すので、promise型を露出させることができてしまうが、
swiftの場合はそういうことが起きないため、promise汚染が起こせない。いい話。
structured concurrency というコンセプトに基づいて作られてて、Kotlinに使われている。
swiftもこれを採用。
-> これを追うと面白そう。
task.withgroupみたいなのもあるらしい。async letで書ける範囲ならそれで書くとすごく楽
ただし宣言的なので、可変性を持たせるような書き方はできない。
同時に走らせるver
// サーバーから User とその Article 最新 10 件の JSON を取得し、
// それらを返す非同期関数 fetchUserWithArticles を実装して下さい。
// ただし、 User と Article の JSON は並行して取得するものとし、
// User と Artcile の取得には fetchUser および
// fetchArticles を用いるものとします。
import Foundation
import FoundationNetworking
struct User: Identifiable, Codable {
typealias ID = Int
let id: ID
var name: String
var thumbnailURL: URL
}
struct Article: Identifiable, Codable {
typealias ID = Int
let id: ID
var title: String
}
func fetchUser(for id: User.ID) async throws -> User {
let url: URL = URL(string: "https://koherent.org/async-await-challenge/api/user?id=\(id.description)")!
let data: Data = try Data(contentsOf: url)
let user: User = try JSONDecoder().decode(User.self, from: data)
return user
}
func fetchArticles(for userID: User.ID, limit: Int) async throws -> [Article] {
let url: URL = URL(string: "https://koherent.org/async-await-challenge/api/articles?userID=\(userID.description)")!
let data: Data = try Data(contentsOf: url)
let articles: [Article] = try JSONDecoder().decode([Article].self, from: data)
return articles
}
func fetchUserWithArticles(for id: User.ID, limit: Int) async throws -> (user: User, articles: [Article]) {
// 🚧 ここを実装する
async let user = fetchUser(for:id)// ここにawaitをつけると代入前にfetchUserが終わる、なるほど
async let articles = fetchArticles(for: id, limit:limit)
let result = try await(user, articles)// ここでまとめてawaitで剥がして、
return result
}
// そのままトップレベルには書けない。runAsyncAndBlockを使っている。
runAsyncAndBlock {
do {
let (user, articles) = try await fetchUserWithArticles(for: 123, limit: 10)
print(user, articles)
} catch {
print(error)
}
}
@asyncHandler
func bar() {
print(2)
await foo1()
print(4)
await foo2()
print(5)
}
print(1)
bar()
print(3)
これは1,2,3,4,5を出力する
asyncをつけるとこうなる
func bar() async {
print(2)
await foo1()
print(3)
await foo2()
print(4)
}
print(1)
await bar()
print(5)
1,2,3,4,5を出力する。
asyncの根っこになれるものが4つある
トップレベル
@main
@asyncHandler
Task.runDetached
(runAsyncAndBlock)// トップレベルで書けるようになるまでの暫定的な手段が用意されてるんだってさ。
これらを利用して無限async汚染問題に対処ができる。
// ViewController の reloadUserButton が押されたときに
// fetchUser 関数を使ってサーバーから User を取得し、
// userNameLabel.text に取得したユーザーの name を設定するように
// onReloadUserButtonPressed メソッドを完成させて下さい。
import Foundation
import FoundationNetworking
class UIViewController {}
final class UIButton {}
final class UILabel {
var text: String?
}
struct User: Identifiable, Codable {
typealias ID = Int
let id: ID
var name: String
var thumbnailURL: URL
}
func fetchUser(for id: User.ID) async throws -> User {
let url: URL = URL(string: "https://koherent.org/async-await-challenge/api/user?id=\(id.description)")!
let data: Data = try Data(contentsOf: url)
let user: User = try JSONDecoder().decode(User.self, from: data)
return user
}
final class ViewController: UIViewController {
let reloadUserButton: UIButton = .init()
let userNameLabel: UILabel = .init()
let userID: User.ID = 123
// 🚧 このメソッドを完成させる
@asyncHandler
func onReloadUserButtonPressed(_ sender: UIButton) {
if let user = try? await fetchUser(for: userID) {// この関数にasyncがついてないけど@asyncHandlerがあるのでawaitが書ける
userNameLabel.text = user.name
}
}
}
let viewController: ViewController = .init()
viewController.onReloadUserButtonPressed(viewController.reloadUserButton)
別スレッド実行の起点になるだけなので、ストップせずに実行される。
Taskはasync関数にとってのスレッドのような物で、
async関数の中でawaitから帰ってきたら別のスレッドになるが、Taskという概念は継続して同じものが使われる。
async1つに対してTask1つが継続性を維持するために生成されるようなイメージ。
で、async letすると、その実行はchild taskが生成されて実行される。
で、runDetached関数は.cancel()関数を実装しているので、上位から丸ごとキャンセルができる。
うーんこれは便利。goのcontextだ。kotlinでもできるんだろうか?とのこと
// ViewController の cancelReloadingUser ボタンを押すと
// reloadUser ボタンで実行されている User のリロードを
// キャンセルするように ViewController の実装を完成させて下さい。
import Foundation
import FoundationNetworking
class UIViewController {}
final class UIButton {}
final class UILabel {
var text: String?
}
struct User: Identifiable, Codable {
typealias ID = Int
let id: ID
var name: String
var thumbnailURL: URL
}
func fetchUser(for id: User.ID) async throws -> User {
let url: URL = URL(string: "https://koherent.org/async-await-challenge/api/user?id=\(id.description)")!
let data: Data = try Data(contentsOf: url)
let user: User = try JSONDecoder().decode(User.self, from: data)
return user
}
// 🚧 このクラスを完成させる
final class ViewController: UIViewController {
let reloadUserButton: UIButton = .init()
let cancelReloadingUserButton: UIButton = .init()
let userNameLabel: UILabel = .init()
var handle:Task.Handle<Void>?
func onReloadUserButtonPressed(_ sender: UIButton) {
handle = Task.runDetached { [self] in
if let user = try? await fetchUser(for: 123) {
userNameLabel.text = user.name
}
}
}
func onCancelReloadingUserButtonPressed(_ sender: UIButton) {
handle?.cancel()
}
}
let viewController: ViewController = .init()
viewController.onReloadUserButtonPressed(viewController.reloadUserButton)
viewController.onCancelReloadingUserButtonPressed(viewController.cancelReloadingUserButton)
メモ
callback -> 関数を呼び出したスレッドで
dispatchqueue.async() でどこかに投げ込む
・taskがexecutorみたいなのを持てるようになってる
・awaitから戻ってきた時にcontinuationするスレッドをどうするかみたいなのはexecutorの責務になっている。
・どのスレッドに戻るかを設定することができる
actorまわりと関連してるらしい、、わかんね。
actorは自身のメソッドが必ずそこで直列化される?
関数をasync化してみよう
func foo(completion: @escaping) ~ みたいなのがあった時に、
withUnsafeContinuation { continuation in を使って
foo {
continuation.resume(returning: value)
}
}
unsafeとcheckedがある。checkedをつけるとcontinuation.resumeの呼び忘れをエラーにできる。
当然checkedのほうがいろんなコストがあるとのこと。
コールバック関数をasyncでラップしてasync化しよう
// コールバックで結果を受け取る非同期関数 download を使って、
// async で結果を返す非同期関数 download を実装して下さい。
import Foundation
import FoundationNetworking
func download(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
do {
let data: Data = try Data(contentsOf: url)
completion(.success(data))
} catch {
completion(.failure(error))
}
}
// 🚧 ここを実装する
func download(from url: URL) async throws -> Data {
try await withUnsafeThrowingContinuation { continuation in
download(from: url) { result in
continuation.resume(with: result)
}
}
}
runAsyncAndBlock {
do {
let url: URL = URL(string: "https://koherent.org/async-await-challenge/api/user?id=123")!
let data: Data = try await download(from: url)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
}
戻りスレッドとかはどうなっちゃうんだろう
UIスレッドに指定したい、とかはありそうだけど、それはどうやるんだろう。
-> GDCではい。まああれスッゲー便利だもんな